/**
 * Auteur : Dominique GUERIN
 * dominiquephDOTguerinATgmailDOT..
 * dominiqueDOTguerinATinseeDOTfr(ance)  
 * Remerciements: � Keith Brown pour ses deux livres sur la s�curit� sous Windows et ses diff�rents articles
 *                 et � Michel Barnett de Microsoft pour ses deux articles 
 *                ".NET Remoting Authentication and Authorization Sample"  et le code qui les accompagne
 *  Le code est utilisable, modifiable et redistribuable � volont� sous la condition de ne pas supprimer ces 7 lignes.
 */ 
 
 package fr.doume.tcp.sspi;

//tomcat 5.5
//import org.apache.commons.logging.Log;
//import org.apache.commons.logging.LogFactory;

//tomcat 6
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import java.util.Date;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;

import fr.doume.tcp.authenticator.Parameters;
import fr.doume.tcp.authenticator.AuthenticationContext;
import fr.doume.tcp.sspi.SSPException;
import fr.doume.tcp.sspi.ConnectionTcpPool;
import java.io.IOException;
import java.lang.Short;

import java.net.InetAddress;
import java.net.InetSocketAddress;

import java.io.OutputStream;
import java.io.InputStream;
import java.net.Socket;
import java.util.Arrays;

import java.util.Timer;
import java.util.TimerTask;

import javax.net.ssl.SSLSocketFactory;

  /**
  * Translation of the tomcat roles into SIDs.
  * These SIDs are compared with the sids in the access token, after each authentication
  */
public class TranslationRolesIntoSids
{
	private static Log log = LogFactory.getLog(TranslationRolesIntoSids.class);
	/**
	 * Time-out used by the connection and the calls to Read and Write of the socket 
	 */
	private static final int TIME_OUT_MS =4000;
/**
* arrray of sids send by getSids
*/  
	private byte[][] sids_array;
	/**
	 * <code>sids_array</code> is copied into <code>sids</code>.
	 * Will be used by the class <code>SSPAuthenticatio</code>, to know the list of SIDs
	 * in the access token of a client after an authentication.
	 */
	private byte[] sids;
  /**
   * Socket used with NegoServer.
   */
  private Socket s = null;
  
  /** 
   * Timer: used to close a socket whne the connection, the calls of REad and Write.
   */
  private Timer stop = null;
  
  private int sidsCount;
  
  /**
  * array of groups given before the translation.
  */  
	private String[] groups;

	/**
	* used by a cache of the translation
	*/
	private Date creationDate;

	private boolean translated;


  private Parameters parameters;
  
  private AuthenticationContext authenticationContext;
  /**
  *  Initialize the private fields. <code>sids</code> et <code>groups</code> are defined as array with a length = 0, 
  * <code>creationDate</code> is the Date of the creation of the instance, and <code>translated</code> is set to false.
  */
	public TranslationRolesIntoSids( AuthenticationContext ac)
	{
		
		parameters = ac.getParameters();
		authenticationContext = ac;
		sids = new byte[0];
		sidsCount = 0;
		groups= new String[0];
		creationDate = new Date();
		translated = false;
	}

  /**
  *  This method calls <code>translateToSids</code> when translated is false, 
  * then calls <code>verifySids</code>.
  * After that <code>tarnslated</code> is set to true.
  * NegoServer returns an arry of SIDs, each SID is an array of bytes.
  * This array is only used by calls to the Windows API.
  */
	public int getSidsCount(String[] roles)
	throws SSPException
	{
    if (log.isDebugEnabled()) log.debug("Begin");
    if(!translated) 
    {
      if (log.isDebugEnabled()) log.debug("! translated");
      //if (sids.length == 0 && roles.length != 0)
      {
        if (! translateToSids( roles) )
        {
          throw new SSPException("Translation from roles to sids failed");
        }
        verifySids();
      }
      groups = roles;
      translated = true;
      if( roles.length != sidsCount )
      {
        throw new SSPException("Count of tomcat roles != count of sids");
      }
    }//if(!translated)
		return sidsCount;
	}
	
	public String[] getGroups()
	{
    return groups;
  }
  
	public int getSidsCount()
	throws SSPException
	{
		if (sidsCount == 0 ){
      if( !translated)
			throw new SSPException("Count of roles is zero");
		}
		return sidsCount;
	}
	/**
	* Reurns a bytes array (the SIDs)
	*/
	public byte[] getSids()
	throws SSPException
	{
		if (sidsCount == 0 ){
      if( !translated)
			throw new SSPException("Count of roles is zero");
		}
		return sids;
	}

	/**
	* Returns the creation Date of this instance
	*/
	public Date getDate()
	{
    return creationDate;
	}

  /**
  * Call NegoServer With an array of String (names of the groups).
  * Returns <code>false</code> if there is an error. 
  * In this case, the public method <code>getSids(roles)</code> throw an Exception. <br>
  * The array of SIDs is found by NegoServer :
  * <pre>
             <code>NTAccount temp = new NTAccount(new string(groups_with_headers, (int)position, (int)size_elem));
             si = (SecurityIdentifier)temp.Translate(typeof(SecurityIdentifier));</code>
  * </pre>
  * <pre>
  * Format of the message sent to Negoserver: (in BIG ENDIAN Unicode)
  *     count of char or short sent
  *     count of roles (short)
  *     for each role:
  *        length of the role (short)
  *        name of the role (array of char)
  * Before this message, there is the byte 0 : NegoServer translate the groups into sids.
  * If a socket is reused, you send before the byte 2
  * </pre> 
  */	
  private boolean translateToSids( String[] roles)
  throws SSPException
  {
  try
    {
      long len_buf_char = 0;
      for (int i = 0; i < roles.length; i++)
      {
        len_buf_char += (roles[i].length() + 1);  // len + 1 slot with the len of the string
      }
      len_buf_char += 2; // add the two headers : count of roles and the size of the char[] sent
      if(len_buf_char > Short.MAX_VALUE)
      {
        throw new SSPException("The length sent must be smaller than Short.MAX_VALUE");
      }
      long len_buf_bytes = len_buf_char * 2;
      if (log.isDebugEnabled()) log.debug("sizeof liste of roles sent to the windows server: " + len_buf_char);
      ByteArrayOutputStream buf = new ByteArrayOutputStream((int)len_buf_bytes + 2);
      DataOutputStream data = new DataOutputStream( buf);
      data.writeByte(2);  // send 2 when a socket is reused ( (cf NegoServer ConnectionType.ReuseAConnectionTcp)
      data.writeByte(0);  // calls Negoserver with 0 : We calls Negoserver to do a translation of the roles into SIDs.  
                          //        (cf NegoServer ConnectionType.TranslationNameGroupsToSids)
      data.writeShort((short) (len_buf_char -1));
      data.writeShort( (short) (roles.length) );
      for (int i = 0; i <roles.length ; i++)
      {
        data.writeShort( (short) roles[i].length() );
        data.writeChars( roles[i]);
      }
      data.flush();
      byte[] toSend = buf.toByteArray();
      return connect( toSend );
    }
    catch(IOException e)
    {
      throw new SSPException("IOException: " + e.getMessage());
    }
    
    //return true;
    
  }
  /**
   * Called by <code>translateToSids</code>
   * Try to use an older connection, else calls <code>createNewConnection</code>.
   * Sends an array of tomcat roles (Unicode char  BIG_ENDIAN),
   * then calls the methods <code>read_header</code> and <code>read_other_bytes</code>.
   */
   
  private boolean connect( byte[] toSend)
  throws IOException, SSPException
	{
		ConnectionTcpPool pool = null;
		boolean well = false;
    OutputStream out;

    try
    {
    
      AuthenticationContext ac = getAuthenticationContext();
      pool = ac.getSocketsPool();
      s = pool.getSocket();

      while( s != null)
      {
        out = s.getOutputStream();
        try
        {
          stop = new Timer(); // set the time-out...
          stop.schedule( new CancelConnection() , TIME_OUT_MS);
          out.write( toSend, 0, toSend.length);
          break;            
        }
        catch(IOException ex)
        {
          cancelStopAndCloseSocket();
          s = pool.getSocket();
        }
      }
      if ( s == null)  // no socket in the pool
      {
        Parameters p = ac.getParameters();
        createNewConnection( p );
        out = s.getOutputStream();
        stop = new Timer(); 
        stop.schedule( new CancelConnection() , TIME_OUT_MS);
        
        // New Socket : we do not send the first byte because we do not reuse an older socket
      
        out.write( toSend, 1, toSend.length - 1);
      }
      cancelstop();     
      
      if (log.isDebugEnabled()) log.debug("List of roles sent to the windows server");    
      
      InputStream input = s.getInputStream();     
      
      //read the header wich contains the length sent by the server
      byte[] header = new byte[2];
      int count_of_bytes_to_read = read_header( input, header);

      if (count_of_bytes_to_read == 0)
      { 
        sidsCount = 0;
        sids = header;
      }
      //count of sids on two bytes
      else if (count_of_bytes_to_read < 2)
      {
        throw new SSPException("Sids sent by the windows server are inconsistent");
      }
      
      else
      {
        sids = read_other_bytes( count_of_bytes_to_read, input, header);
      }
      if (log.isDebugEnabled()) log.debug("Count of read bytes : " + count_of_bytes_to_read);
      well = true;
    }   
    finally
    {
      cancelstop();
      if (! well) closeSocket();
      else
      {
        pool.add(s);
        s = null;
      }
    }      
    return true;
	}
  /**
   * Try to cretae a new connection
   */
  private void createNewConnection(Parameters p)
  throws IOException,SSPException
  {  
    InetAddress adresse_server;
    int translationport;
    InetSocketAddress socket_address;

    if (log.isDebugEnabled()) log.debug("To the translation Roles to Sids : new Socket");
    adresse_server = p.getInetAddressServer();
    translationport = p.getPort();
    socket_address  = new InetSocketAddress(adresse_server, translationport);

    if (log.isDebugEnabled()) log.debug("Before new Socket");
    s = new Socket();
    if (log.isDebugEnabled()) log.debug("After new Socket");
    s.setSoTimeout(TIME_OUT_MS);
    s.connect(socket_address, TIME_OUT_MS);

    String server = p.getWindowsServerName();
    if ( p.isSSLRequired() )
    {
    //SSL
      if (log.isDebugEnabled()) log.debug("SSLRequired");
      if (log.isDebugEnabled()) log.debug("server name : " + server);
      try
      {
        SSLSocketFactory factory = p.getSSLFactory();
        s = factory.createSocket(s, server, translationport,true);
        if (log.isDebugEnabled()) log.debug("SSL is used to send and receive the flow to authentication");
      }  
      catch(Exception e)
      { 
        throw new SSPException( "factory " +e.getMessage());
      }
    }
  }
	private void closeSocket()
	{
	   if (s != null)
	   {
      try 
       {
        s.close();
       }
       catch(IOException e){}
       s = null;
	   }
	}
	/**
	 * Cancels the timer
	 */
	private void cancelstop()
	{
    try
    {
      if (stop != null)
      {
        stop.cancel();
      }
    }
    catch(IllegalStateException e){}
    catch(IllegalArgumentException e){}
    finally
    {
      stop = null;
    }
	}
	
	private void cancelStopAndCloseSocket()
	{
    cancelstop();
    closeSocket();
  }
  /**
   * Read two bytes to know the count of byes to read
   */
   
  private int read_header( InputStream input, byte[] header)
  throws IOException, SSPException
  {
       //read the header wich contains the length sent by the server

    if( header.length != 2 )
    {
      throw new SSPException("Application is Inconsistent");
    }
    int chunk_size;
    int position = 0;
    int remaining_to_read = header.length;
    
    if (log.isDebugEnabled()) log.debug("remaining_to_read: " + remaining_to_read); 
    do
    {
      chunk_size = input.read(header, position, remaining_to_read);
      
      if (log.isDebugEnabled()) log.debug("chunk_size: " + chunk_size);
      if (chunk_size == -1) break;
      
      remaining_to_read -= chunk_size;  
      if (log.isDebugEnabled()) log.debug("remaining_to_read: " + remaining_to_read); 
      
      position += chunk_size; 
    } while (remaining_to_read != 0);

    if (log.isDebugEnabled()) log.debug("remaining_to_read: " + remaining_to_read + "   ,chunk_size: " + chunk_size );    
    
    if ( remaining_to_read != 0)  // => chunk_size == -1
    {
      throw new SSPException("read_header: Sids sent by the windows server are inconsistent");
    }
    
    int highByte = header[0];
    highByte &= 0xff;
    highByte = highByte << 8;
    int lowByte = ((int)header[1]) & 0xff;
    int count_of_bytes_to_read = highByte + lowByte;

    if (log.isDebugEnabled()) log.debug("chunk_size: " + chunk_size + "   ,count_of_bytes_to_read: " + count_of_bytes_to_read ); 
    
    //if ( chunk_size == 0 && count_of_bytes_to_read !=0 ) // !!??
    //{
    //  throw new SSPException("read_header 2: Sids sent by the windows server are inconsistent");      
    //}
    return count_of_bytes_to_read;
  }
  /**
   * Read the other bytes after the header cf: <code>read_header</code> 
   */
  private byte[] read_other_bytes(int count_of_bytes_to_read, InputStream input, byte[] header)
  throws IOException, SSPException
  {
    int chunk_size;
    int position = header.length;
    int remaining_to_read = count_of_bytes_to_read;
    byte[] received = new byte[ header.length + count_of_bytes_to_read ];
    do
    {
      chunk_size = input.read(received, header.length, remaining_to_read);
      if (chunk_size == -1) break;
      remaining_to_read -= chunk_size;  
      position += chunk_size;
    } while (remaining_to_read != 0);
 
    if (remaining_to_read != 0) //  => chunk_size == -1
    {
      if (log.isDebugEnabled()) log.debug("remaining_to_read: " + remaining_to_read + "   ,chunk_size: " + chunk_size );    
      throw new SSPException("read_other_bytes: Sids sent by the windows server are inconsistent"); 
    }
    received[0] = header[0];
    received[1] = header[1];
    
    int len = received[2];
    len &= 0xff;
    len = len << 8;
    int temp =  ( (int) received[3]) & 0xff;
    len += temp;
                           
    sidsCount = len;
    return received;

  }
  /**
  * Verify the bytes sent by NegoServer:
  * Negoserver returns a length and after the data.
  * The data conatains the count of elements and then each element.
  * Each elemnet contains the length of the SID and then the SID.
  */
  private void verifySids()
  throws SSPException
  {
    // Negoserver returns the length of the list of SIDs and then, the list
    int size_of_sids = ValueOfTwoBytesInSids(0);
    int sidslen = sids.length;
    
    if (log.isDebugEnabled()) log.debug("sidslen: " + sidslen + "   ,size_of_sids: " + size_of_sids ); 
    if ( sids.length != (size_of_sids + 2) ) 
    {
      throw new SSPException("Sids sent by the windows server are inconsistent : sids.length != (size_of_sids + 2) "); 
    }
    int position = 0;
    if (size_of_sids == position) return;

    position +=2;
    if (size_of_sids < position) throw new SSPException("Sids sent by the windows server are inconsistent : size_of_sids < position");   
    int count_of_sids = ValueOfTwoBytesInSids(position );
    
    sids_array = new byte[count_of_sids][];
    
    // Each Sid contains a length and an arry of bytes
    int sizeof_a_sid;
    for (int i = 0; i < count_of_sids; i++)
    {
      if (log.isDebugEnabled()) log.debug("index : " + i + "   ,position: " + position ); 
      position +=2; 
      if (size_of_sids < position) throw new SSPException("Sids sent by the windows server are inconsistent : size_of_sids < position");
      sizeof_a_sid = ValueOfTwoBytesInSids(position );
      
      
      //Arrays.copyOfRange exists in the JDK 6 mais not in the JDK 5
      //byte[] sid = Arrays.copyOfRange( sids, position+2, sizeof_a_sid + position+2);
      byte[] sid = copyarray( sids, position+2, sizeof_a_sid + position+2);
      sids_array[i] = sid; 
      
      if (log.isDebugEnabled()) log.debug("indice : " + i + "   ,sizeof_a_sid: " + sizeof_a_sid ); 
      position += sizeof_a_sid;
      if (size_of_sids < position) throw new SSPException("Sids sent by the windows server are inconsistent : size_of_sids < position");
    }   
    if (size_of_sids != position) throw new SSPException("Sids sent by the windows server are inconsistent : size_of_sids < position");
    
  }
  
  /**
  * Arrays.copyOfRange does not exist in the dans le JDK 6 mais pas dans le JDK 5: Remplace cette methode pour la compatilit� ascendante 
  */
  private byte[] copyarray(byte[] source, int from, int to)
  // replace Arrays.copyOfRange from jdk 6 for jdk 5
  {
    if ( from < 0  || from > source.length )
    {
      throw new java.lang.ArrayIndexOutOfBoundsException();
    }
    if ( from > to )
    {
      throw new java.lang.IllegalArgumentException();
    }
    
    byte[] target = new byte[to-from];
    for (int i = 0; i< to - from; i++)
    {
      target[i] = source[i + from];
    }
    return target;
  }

  /**
   * Get an int
   */ 
  private int ValueOfTwoBytesInSids(int position)
  {
    int highByte, lowByte;
    
    int pos_next = position++;
    
    highByte = sids[pos_next];
    highByte &= 0xff;
    highByte = highByte << 8;
    lowByte = ((int)sids[position]) & 0xff;
    
    return highByte + lowByte;
  }

  public AuthenticationContext getAuthenticationContext()
  {
    return authenticationContext;
  }

  /**
  * Defines the method run after the time out.
  */
  protected class CancelConnection extends TimerTask
  {
    public void run()
    {
      if ( s!= null)
      {
          try
          {
            s.close();
          }
          catch(IOException e){}
          finally
          {
            s = null;          
          }
      }
    }
    
  }
  

}

